﻿using System;
using System.Diagnostics.Contracts;
using System.Linq;
using StyleCop;
using StyleCop.CSharp;

namespace Jsl.StyleCop.CSharp.OrderingRules
{
    /// <summary>
    /// Verifies that members are in alphabetical order.
    /// </summary>
    public class MembersMustBeInAlphabeticalOrder : CsRuleBase<SourceAnalyzer>
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="MembersMustBeInAlphabeticalOrder"/> class.
        /// </summary>
        /// <param name="analyzer">The analyzer.</param>
        public MembersMustBeInAlphabeticalOrder(RuleAnalyzer analyzer)
            : base(analyzer)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Analyzes the document.
        /// </summary>
        public override void AnalyzeDocument()
        {
            this.Document.WalkDocument(this.AnalyzeElement);
        }

        /// <summary>
        /// Analyzes the element.
        /// </summary>
        /// <param name="element">The element to analyze.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>Always <c>true</c>.</returns>
        public bool AnalyzeElement(CsElement element, CsElement parentElement, object context)
        {
            if (MembersMustBeInAlphabeticalOrder.ShouldProcessElement(element))
            {
                this.VerifyElementIsInCorrectOrder(element, parentElement);
            }

            return true;
        }
        #endregion Public Methods

        #region Private Methods
        private static string GetBaseElementName(CsElement element)
        {
            var method = element as Method;
            if (method == null)
            {
                return element.Name;
            }

            // The method name includes the generic type parameters is any. Hack them off the end so we can compare names.
            var genericTypeCharacter = method.Name.IndexOf('<');
            if (genericTypeCharacter < 0)
            {
                return method.Name;
            }
            else
            {
                return method.Name.Substring(0, genericTypeCharacter);
            }
        }

        private static bool ShouldProcessElement(CsElement element)
        {
            if (element.Generated)
            {
                return false;
            }

            // Process any element that should be ordered relative to its siblings.
            var type = element.ElementType;
            return type == ElementType.Class
                || type == ElementType.Constructor
                || type == ElementType.ConstructorInitializer
                || type == ElementType.Delegate
                || type == ElementType.Destructor
                || type == ElementType.Enum
                || type == ElementType.Event
                || type == ElementType.ExternAliasDirective
                || type == ElementType.Field
                || type == ElementType.Indexer
                || type == ElementType.Interface
                || type == ElementType.Method
                || type == ElementType.Namespace
                || type == ElementType.Property
                || type == ElementType.Struct;
        }

        private void VerifyElementIsInCorrectOrder(CsElement element, CsElement parentElement)
        {
            Contract.Assume(parentElement.ChildElements != null);

            var siblingsThatShouldBeBeforeThisElement =
                from siblingElement in parentElement.ChildElements
                let baseSiblingName = GetBaseElementName(siblingElement)
                where element.ElementType == siblingElement.ElementType
                    && element.IsStatic() == siblingElement.IsStatic()
                    && element.IsReadOnly() == siblingElement.IsReadOnly()
                    && element.ActualAccess == siblingElement.ActualAccess
                    && string.Compare(GetBaseElementName(element), baseSiblingName, StringComparison.OrdinalIgnoreCase) > 0
                    && element.Location.StartPoint.Index < siblingElement.Location.StartPoint.Index
                orderby baseSiblingName descending
                select siblingElement;

            var closestSibling = siblingsThatShouldBeBeforeThisElement.FirstOrDefault();
            if (closestSibling != null)
            {
                this.AddViolation(
                    element, 
                    RuleAnalyzer.MembersMustBeInAlphabeticalOrder,
                    GetBaseElementName(element),
                    GetBaseElementName(closestSibling));
            }
        }
        #endregion Private Methods
    }
}